/* The MIT License:

Copyright (c) 2015 Ivan Gagis <igagis@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. */

// Home page: http://stob-java.googlecode.com

package org.stobml;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

/**
 *
 * @author ivan
 */
public class STOBNode {
    private List<STOBNode> children = null;

    private void CreateChildrenCollection(){
        this.children = new ArrayList<STOBNode> ();
    }

    public String value;

    public STOBNode (){}

    public STOBNode(String value){
        this.value = value;
    }

    public List<STOBNode> children(){
        return this.children;
    }

    public void removeAll(){
        this.children = null;
    }

    public void add(STOBNode node){
        if (this.children == null) {
            this.CreateChildrenCollection ();
        }

        this.children.add (node);
    }

    public void remove(STOBNode node){
        if (this.children == null) {
            return;
        }
        this.children.remove (node);
    }

    public STOBNode find(String v){
        if (this.children == null || v == null) {
            return null;
        }
        for(STOBNode n : this.children){
            if (v.equals (n.value)) {
                return n;
            }
        }
        return null;
    }

    public static class NodeNotFoundException extends Exception{
    }
    
    public STOBNode get(String v) throws NodeNotFoundException{
        STOBNode n = this.find (v);
        if (n == null) {
            throw new NodeNotFoundException();
        }
        return n;
    }

    public float asFloat(){
        return Float.valueOf(this.value);
    }

    public boolean asBool(){
        return "true".equals (this.value);
    }

    public int asInt(){
        return Integer.valueOf(this.value);
    }
    
    public double asDouble(){
        return Double.valueOf(this.value);
    }
    
    public long asLong(){
        return Long.valueOf(this.value);
    }

    //TODO:

    public static List<STOBNode> load(InputStream is) throws IOException, STOBParser.FormatException{
        final List<STOBNode> stack = new ArrayList<>();

        stack.add (new STOBNode ());

        STOBParser.Parse(is,
                new STOBParser.Listener() {
                    STOBNode lastParsed = null;
                    
                    @Override
                    public void onStringParsed(String str) {
                        lastParsed = new STOBNode(str);
                        stack.get(stack.size() - 1).add(lastParsed);
                    }

                    @Override
                    public void onChildrenParseStarted() {
                        assert(lastParsed != null);
                        stack.add(lastParsed);
                    }

                    @Override
                    public void onChildrenParseFinished() {
                        stack.remove(stack.size() - 1);
                    }
                }
            );
        
        assert (stack.size() == 1);
        return stack.get(0).children();
    }
    
    public static void write(OutputStream os, List<STOBNode> chain, boolean formatted) throws IOException{
        writeInternal(os, chain, formatted, 0);
    }
    
    private static void writeInternal(OutputStream os, List<STOBNode> chain, boolean formatted, int indentation) throws IOException{
        byte quote = (byte)'"';
        byte lcurly = (byte)'{';
        byte rcurly = (byte)'}';
        byte space = (byte)' ';
        byte tab = (byte)'\t';
        byte newLine = (byte)'\n';

        //used to detect case of two adjacent unquoted strings without children, need to insert space between them
        boolean prevWasUnquotedWithoutChildren = false;

        boolean prevHadChildren = true;

        CanStringBeUnquotedRet csbu = new CanStringBeUnquotedRet();
        
        for(STOBNode n : chain){
            //indent
            if(formatted){
                for(int i = 0; i != indentation; ++i){
                    os.write(tab);
                }
            }

            //write node value

            boolean unqouted = canStringBeUnquoted(n.value, csbu);

            if(!unqouted){
                os.write(quote);

                if(csbu.numEscapes == 0){
                    if(n.value != null){
                        os.write(n.value.getBytes());
                    }
                }else{
                    os.write(makeEscapedString(n.value).getBytes());
                }

                os.write(quote);
            }else{
                boolean isQuotedEmptyString = false;
                if(n.value == null || n.value.length() == 0){//if empty string
                    if((n.children() == null || n.children().size() == 0) || !prevHadChildren){
                        isQuotedEmptyString = true;
                    }
                }

                //unquoted string
                if(!formatted && prevWasUnquotedWithoutChildren && !isQuotedEmptyString){
                    os.write(space);
                }

                if(n.value == null || n.value.length() == 0){//if empty string
                    if(isQuotedEmptyString){
                        os.write(quote);
                        os.write(quote);
                    }
                }else{
                    assert(csbu.numEscapes == 0);
                    assert(n.value != null);
                    os.write(n.value.getBytes());
                }
            }

            prevHadChildren = n.children() != null && n.children().size() != 0;
            if(n.children() == null || n.children().size() == 0){

                if(formatted){
                    os.write(newLine);
                }
                prevWasUnquotedWithoutChildren = (unqouted && (n.value != null && n.value.length() != 0));
                continue;
            }else{
                prevWasUnquotedWithoutChildren = false;
            }

            if(!formatted){
                os.write(lcurly);

                writeInternal(os, n.children(), false, 0);

                os.write(rcurly);
            }else{
                if(n.children() != null && n.children().size() == 1 && (n.children().get(0).children() == null || n.children().get(0).children().size() == 0)){
                    //if only one child and that child has no children

                    os.write(lcurly);
                    writeInternal(os, n.children(), false, 0);
                    os.write(rcurly);
                    os.write(newLine);
                }else{
                    os.write(lcurly);
                    os.write(newLine);
                    writeInternal(os, n.children(), true, indentation + 1);

                    //indent
                    for(int i = 0; i != indentation; ++i){
                        os.write(tab);
                    }
                    os.write(rcurly);
                    os.write(newLine);
                }
            }
        }//~for()
    }
    
    private static class CanStringBeUnquotedRet{
        public int numEscapes;
    }
    
    private static boolean canStringBeUnquoted(String s, CanStringBeUnquotedRet out_ret){
        out_ret.numEscapes = 0;

        if(s == null || s.length() == 0){//empty string is can be unquoted when it has children, so return true.
            return true;
        }

        boolean ret = true;
        for(int i = 0; i != s.length(); ++i /*, ++out_ret.length*/){
            char c = s.charAt(i);
            switch(c){
                case '\t':
                case '\n':
                case '\r':
                case '\\':
                case '"':
                    ++out_ret.numEscapes;
                case '{':
                case '}':
                case ' ':
                    ret = false;
                    break;
                default:
                    break;
            }
        }
        return ret;
    }

    private static String makeEscapedString(String str){
        if(str == null){
            return "";
        }
        
        StringBuilder ret = new StringBuilder();
        
        for(int i = 0; i != str.length(); ++i){
            char c = str.charAt(i);

            switch(c){
                case '\t':
                    ret.append("\\t");
                    break;
                case '\n':
                    ret.append("\\n");
                    break;
                case '\r':
                    ret.append("\\r");
                    break;
                case '\\':
                    ret.append("\\\\");
                    break;
                case '"':
                    ret.append("\\\"");
                    break;
                default:
                    ret.append(c);
                    break;
            }
        }
        return ret.toString();
    }
    
    public static boolean compareChains(List<STOBNode> a, List<STOBNode> b){
        if(a == null && b == null){
            return true;
        }
        if(a == null || b == null){
            return false;
        }
        
        if(a.size() != b.size()){
            return false;
        }
        
        for(int i = 0; i != a.size(); ++i){
            STOBNode an = a.get(i);
            STOBNode bn = b.get(i);
            
            if(!compareChains(an.children(), bn.children())){
                return false;
            }
            
            if((an.value == null || an.value.length() == 0) && (bn.value == null || bn.value.length() == 0)){
                continue;
            }
            if(an.value == null || bn.value == null){
                return false;
            }
            if(!an.value.equals(bn.value)){
                return false;
            }
        }
        
        return true;
    }
}
